Skip to content

Add Finnish (fi) translations - #142

Merged
methodofaction merged 4 commits into
mainfrom
claude/finnish-language-translations-608db0
Aug 25, 2026
Merged

Add Finnish (fi) translations#142
methodofaction merged 4 commits into
mainfrom
claude/finnish-language-translations-608db0

Conversation

@methodofaction

@methodofaction methodofaction commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Adds Finnish as a supported language.

What changed

New locale in locales/fi/, mirroring the structure of the existing non-English locales (da/de/nl/no):

File Keys
app.yml 519 — reuses the YAML anchors so invoice translations merge into the payment, delivery and order documents
countries.yml 249 country names
currencies.yml 166 currency names
units.yml 57 symbol-less units

Plus the wiring needed for it to actually ship and be covered:

  • locales/locales.go — added fi to the //go:embed directive. Without this the files are silently excluded from the binary. Also adds Codes(), which derives the supported-language list from the embedded content so it only has to be maintained in one place.
  • components/t/units_test.go — the two hardcoded locale slices now come from locales.Codes(), so an embedded locale can't be silently untested.

Key parity guard

locales/locales_test.go is new, prompted by review:

  • TestLocaleKeyCoverage checks every locale against the English key set plus the union of currency codes (English deliberately has no currencies.yml). regimes.* and foreign proper nouns like Codice fiscale are exempt by rule; everything else that falls back to English today is listed per-locale in knownGaps. A new locale must ship with no entry there, and a stale entry also fails, so the list can only shrink.
  • TestLocaleFilesUseOwnCode catches a file copied from another locale with the original root key left behind — a mistake that would otherwise load silently under the wrong language.

Writing the test surfaced pre-existing gaps in other locales. They're recorded in knownGaps and left alone as out of scope, but three are bugs rather than missing translations: fr spells three payment-term keys with hyphens so they never match, pl has prices_include_tax where the template asks for prices_include, and de/it/pl are each missing a country code.

Notes for reviewers

  • Units use the partitive singular, since they render directly after a numeral: "20 tuntia", "5 kappaletta", "2 laatikkoa". (An earlier version of this description said "plural" — the forms were always right, the label wasn't.)
  • fi translates five keys the da/nl/no family leaves to the English fallback: org.party.person, person_label, website, website_label and billing.invoice.title.adjustment. These aren't regime-specific — the renderer reaches for them in any locale, and a Finnish purchase order was printing "Web: https://…" in English.
  • Regime-specific titles still fall back to English. The regimes.* keys (PT document codes, GR/CO/IT/SG titles, PL KSeF) are not translated, so a PT delivery note renders "Delivery note". This matches every non-English locale except es.
  • org.party.labels.default is "ALV-tunnus" (VAT number) rather than a literal rendering of English "TIN", following da/de/nl.
  • No LocaleForExample case was added in internal/gallery/gallery.go — there's no fi- prefixed example, so it would be dead code. It's a one-line case "fi" whenever a Finnish example lands, since the prefix is a country code and FI happens to equal fi.

Testing

go vet ./... and the full suite pass. I rendered invoice, credit note, purchase order, delivery note, adjustment credit note and multi-method payment receipt examples with WithLocale("fi") and read the output text. The new parity test was checked against deliberate breakage rather than assumed correct: removing a key from fi/app.yml fails it, re-keying fi/app.yml as da: fails it, and a stale knownGaps entry fails it.

🤖 Generated with Claude Code

Adds a complete Finnish locale following the structure of the existing
non-English locales (da/de/nl/no), with exact key parity against them:

- app.yml: 514 keys, reusing the YAML anchors so invoice translations
  merge into the payment, delivery and order documents
- countries.yml: 249 country names
- currencies.yml: 166 currency names
- units.yml: 57 symbol-less units in partitive plural, so they read
  correctly after a quantity ("20 tuntia", "5 kappaletta")

Also registers the locale so it actually ships and is covered:

- locales.go: add fi to the //go:embed directive, without which the
  files are silently excluded from the binary
- units_test.go: add "fi" to both locale lists so the unit coverage
  tests apply to it

Like the other non-English locales apart from es, the regime-specific
titles (regimes.*) are left untranslated and fall back to English.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@methodofaction
methodofaction requested a review from rilla August 17, 2026 08:22

@rilla rilla left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice one — the wiring is right and the Finnish reads like Finnish. I flattened all four files (anchors and merge keys resolved) and diffed key sets: exact 986-key match with da/nl/no, anchors on the same lines, so it really is a faithful mirror. Also rendered all 81 examples with ?locale=fi and grepped for missing-key markers — zero. Plurals are fine too: every locale gets the same one/other rule, which is what Finnish wants, and both forms are present.

Left inline suggestions for a few word-level things. Three I'd actually take (rate, 6pack, taxable_event), the rest are yours to judge.

Two notes that don't fit on a line:

The units are partitive singular, not partitive plural. "20 tuntia" is singular — the plural would be "tunteja". Every form in the file is correct, it's just the label in the description. Worth fixing so the next person translating a Finno-Ugric locale doesn't copy the wrong rule.

Key parity is this PR's main risk and there's nothing guarding it. The units test has real teeth — loading unmerged so the English fallback can't mask a gap is exactly right — but the other ~900 keys in app/countries/currencies are only covered by hand. The pattern is already there: same locale list, l.Has(...) over a key set derived from en (plus a sibling for currencies, which en doesn't have). Would make the next locale much cheaper. Related: the locale list now lives in three places (embed directive plus two test slices), so a locale that's embedded but missing from the slices is silently untested — deriving it from the embedded FS would close that.

I'd agree with skipping LocaleForExample, though worth noting the prefix there is a country code and FI happens to equal fi, so it's a one-line case "fi" whenever a Finnish example lands.

Comment thread locales/fi/units.yml Outdated
Comment thread locales/fi/units.yml Outdated
Comment thread locales/fi/units.yml Outdated
Comment thread locales/fi/units.yml Outdated
Comment thread locales/fi/app.yml Outdated
Comment thread locales/fi/app.yml Outdated
Comment thread locales/fi/currencies.yml Outdated
Comment thread locales/fi/currencies.yml Outdated
Comment thread locales/fi/currencies.yml Outdated

@rilla rilla left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Left some comments with nits (by Claude, of course, I don't speak Finnish!)

Word-level fixes from review:

- rate: "taksa" -> "taksaa", the only nominative in an otherwise
  partitive file
- one: "yksi" -> "kpl", since a nominative after a numeral is wrong in
  Finnish and "kpl" is what a Finnish invoice prints
- 6pack: "kuuden pakkausta" -> "kuusipakkausta", the established word
- demijohn: "lasipulloa" -> "koripulloa", which is the specific term
- taxable_event: "verollinen" -> "verotettava", matching the Finnish
  text of the VAT directive for the chargeable event
- cheque: "Šekki" -> "Sekki", modern orthography
- CNY "yuan" -> "juan", GBP "Englannin" -> "Britannian" to match the
  country name, TWD word order

Also translates five keys the older locales leave to the English
fallback but which the renderer uses for every document: the party
person and website labels (a Finnish order was rendering "Web:") and
the adjustment invoice title.

Adds TestLocaleKeyCoverage, which checks every locale against the
English key set plus the union of currency codes, with regime and
proper-noun keys exempted and pre-existing gaps listed in knownGaps.
New locales must ship with no entry there. TestLocaleFilesUseOwnCode
catches a file copied from another locale with its code left behind.

The locale list now comes from locales.Codes(), derived from the embed
directive, so an embedded locale can no longer be silently untested.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@methodofaction

Copy link
Copy Markdown
Contributor Author

Thanks — pushed 645bc9e. All nine inline threads are answered and resolved; eight taken as suggested, carboy/demijohn half taken (details on that thread).

Partitive singular — you're right, and the label was wrong in two places. "20 tuntia" is singular; "tunteja" would be the plural. Every form in the file was already the right one, but I'd described the rule wrong in both the PR body and the first commit message. PR body is corrected; the commit message I've left alone rather than rewriting pushed history.

Key parity — added a guard. TestLocaleKeyCoverage in locales/locales_test.go checks every locale against the English key set plus the union of currency codes across locales (since en has no currencies.yml, as you noted). Two exemption mechanisms:

  • exemptKeyregimes.* and proper nouns like Codice fiscale, which are English-by-design.
  • knownGaps — a per-locale list of keys that fall back to English today, all pre-dating the test. A new locale must ship with no entry there, and the test also fails if an entry becomes stale, so the list can only shrink.

I verified it has teeth rather than trusting it: deleting a key from fi/app.yml fails it, and so does a stale knownGaps entry. There's also TestLocaleFilesUseOwnCode, which catches the classic copy-a-locale-and-forget-the-root-key mistake — fi/app.yml re-keyed as da: fails.

And the locale list now comes from locales.Codes(), derived from the embedded FS, so the embed directive is the single source and units_test.go follows it automatically.

Writing that test surfaced real gaps in other locales. They're in knownGaps with comments, and I've left them alone as out of scope, but three look like actual bugs rather than untranslated strings:

  • fr spells three payment-term keys with hyphens — due-date, end-of-month, advance — where the templates ask for due_date, end_of_month, advanced. French payment terms render in English today.
  • pl has totals.prices_include_tax where the template asks for prices_include.
  • de is missing country_names.AW (Aruba); it and pl are missing KY (Cayman Islands). it and pl currency lists both stop at UYU, missing the last 14 codes.

One deviation from the exact da/nl/no mirror you verified. I added five keys those locales don't have: org.party.person, person_label, website, website_label, and billing.invoice.title.adjustment. They aren't regime-specific — the renderer uses them for any locale, and a Finnish purchase order was printing "Web: https://…" in English. It now prints "Verkkosivu:", and an adjustment credit note prints "Oikaisulasku". So fi is now 991 keys against their 986, and it's the only locale with an empty knownGaps entry alongside ar/ca/eu/gl.

🤖 Addressed by Claude Code

methodofaction and others added 2 commits August 24, 2026 15:54
The org.party.labels.* keys added in #143 are country tax ID
abbreviations — CUIT, NIF, RFC, P.IVA — which are used as they are in
every language, so no locale translates them. Only the generic
labels.default is translated. Exempting them by rule keeps the
knownGaps table for genuinely missing translations.

Also composes knownGaps from named groups so each key literal appears
once, and names the locale file constants, both of which goconst was
flagging.

Verified with golangci-lint 2.13.1 against the repo config: 0 issues.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@methodofaction

Copy link
Copy Markdown
Contributor Author

Merged main and pushed a26f96b — CI on the previous push caught two things, both now fixed.

The new org.party.labels.* keys from #143 are exempt, not gaps. No locale translates CUIT, NIF, RFC, P.IVA or GST Reg No. — they're country tax ID abbreviations that stay as they are in every language, so the coverage test now exempts org.party.labels.* by rule and only requires the generic labels.default. That was the test doing its job: it flagged nine new untranslated keys across all fifteen locales the moment main added them, and the right answer happened to be "these are exempt".

goconst. knownGaps is now composed from named groups (personLabels, chequePayment, currencyTail, …) so each key literal appears exactly once, which reads better than the flat table anyway. Verified with golangci-lint 2.13.1 against the repo config locally: 0 issues.

Worth flagging the maintenance shape this implies, since it's a cost the test imposes on everyone rather than just on new locales: adding an English string that locales don't yet translate will now fail CI until it's either translated, exempted, or recorded in knownGaps. That's the intent — the alternative is the silent English fallback — but if you'd rather it not block unrelated PRs, say so and I'll soften it to a report rather than a failure.

🤖 Addressed by Claude Code

@methodofaction
methodofaction merged commit 0642942 into main Aug 25, 2026
2 checks passed
@methodofaction
methodofaction deleted the claude/finnish-language-translations-608db0 branch August 25, 2026 15:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants